package panel.canvas;

import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.List;

import javax.media.opengl.GL2;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCapabilities;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.awt.GLJPanel;

import shape.Light;
import shape.Shape;
import util.Util;
import camera.Camera;

import com.jogamp.opengl.util.GLBuffers;

public class CanvasPanel extends GLJPanel
	implements MouseMotionListener, MouseListener, GLEventListener{
	
	private List<Shape>	shapeList;
	private List<Shape> selectedShapeList;
	private List<Light>	lightList;
	
	private Camera		camera;
	private Shape		lastClickedShape;
	private boolean		inSelectionMode;

	private List<CanvasObserver> observers = new ArrayList<CanvasObserver>();
	private Point mouse;

	public CanvasPanel(GLCapabilities capabilities){
		super(capabilities);
		camera = new Camera(this);
		mouse = new Point(0, 0);

		addMouseListener(this);
		addMouseMotionListener(this);
		addGLEventListener(this);
		
		shapeList 			= new ArrayList<Shape>();
		selectedShapeList 	= new ArrayList<Shape>();
		lightList 			= new ArrayList<Light>();
		
		lastClickedShape = null;
		inSelectionMode = false;
	}
	
	/* ----------------------------------------------------
	 * Shape functions
	*/
	
	protected void draw( GL2 gl ) {
		camera.apply(gl);
		
		Light.enableLights(gl);
		for (Light light: lightList) {
			light.apply(gl);
		}
		
		for(Shape object : shapeList) {
			object.display(gl, true);
		}
	}

	public void addShape( Shape obj ) {
		shapeList.add(obj);

		repaint();
	}
	
	public void addLight( Light light ) {
		lightList.add(light);
		shapeList.add(light);
		
		repaint();
		notifyObservers();
	}
	
	public void removeShape( Shape obj ) {
		shapeList.remove(obj);
		selectedShapeList.remove(obj);

		repaint();
		notifyObservers();
	}
	
	public void removeLight( Light light ) {
		shapeList.remove(light);
		lightList.remove(light);
		selectedShapeList.remove(light);
		light.release();

		repaint();
		notifyObservers();
	}
	
	public void removeSelectedShapes() {
		for (Shape s: selectedShapeList) {
			shapeList.remove(s);
		}
		
		selectedShapeList.clear();
		
		repaint();
		notifyObservers();
	}
	
	public void selectShape( Shape obj, boolean select ) {
		if (obj.isSelected() != select) {
			obj.setSelected(select);
			if (select)
				selectedShapeList.add(obj);
			else
				selectedShapeList.remove(obj);
			
			repaint();
			notifyObservers();
		}
	}
	
	public Shape[] getSelectedShapes() {
		Shape[] list = new Shape[selectedShapeList.size()];
		selectedShapeList.toArray(list);
		return list;
	}
	
	public Light[] getLights() {
		Light[] list = new Light[lightList.size()];
		list = lightList.toArray(list);
		return list;
	}
	
	public int getSelectedShapeCount() {
		return selectedShapeList.size();
	}
	
	public Camera getCamera() {
		return camera;
	}
	
	private Shape findClickedObject( GL2 gl, Point point ){
		Shape clickedObject = null;
		
		/*
		 * Procedure:
		 * This method will pseudo-render all shapes in GL_SELECT
		 * mode and zoomed to a small square around the clicked point.
		 * 
		 * The frame buffer will not be affected. But, in the end,
		 * glRenderMode(GL_RENDER) will return the number of shapes
		 * that would be drawn, and some data in the int buffer set
		 * with glSelectBuffer().
		 */
		
		int hits;
		int view[] = new int[4];
		IntBuffer buffer = GLBuffers.newDirectIntBuffer(1024);
		
		// Get required information
		point.y = getHeight() - point.y;
		gl.glGetIntegerv(GL2.GL_VIEWPORT, view, 0);
		
		// Setup OpenGL for selection
		gl.glSelectBuffer(64, buffer);
		gl.glRenderMode(GL2.GL_SELECT);
		gl.glInitNames();
		gl.glPushName(0);
		
		// Setup projection matrix
		gl.glMatrixMode(GL2.GL_PROJECTION);
		gl.glPushMatrix();
		gl.glLoadIdentity();
		
		Util.glu.gluPickMatrix( point.x, point.y, 5.0, 5.0, view, 0 );
		Util.glu.gluPerspective( camera.getFieldOfView(), getWidth() * 1.0 / getHeight(),
							camera.getCloseDistance(), camera.getFarDistance() );
		camera.setView(gl);
		
		// Setup model view matrix for rendering
		gl.glMatrixMode(GL2.GL_MODELVIEW);
		gl.glClear( GL2.GL_COLOR_BUFFER_BIT | GL2.GL_DEPTH_BUFFER_BIT );

		// Render objects
		for(int i = 0; i < shapeList.size(); i++) {
			gl.glLoadName(i);
			shapeList.get(i).display(gl, false);
		}
		
		// Process hits
		hits = gl.glRenderMode(GL2.GL_RENDER);
		
		// Get closest shape
		
		/*
		 * glRenderMode returns an array with 4 values for each
		 * "drawn" shape:
		 * 
		 * 	0 - Shape index number (in this buffer)
		 *  1 - Min depth value (distance from camera)
		 *  2 - Max depth value (distance from camera)
		 *  3 - Shape name (used in glLoadName() )
		 *  
		 * Remember that buffer[0..3] are for shape #1, buffer[4..7]
		 * are for shape #2 and so on. 
		 */
		
		if (hits > 0) {
			int minid = 0;
			double min = -1;
			double z;
			for (int i = 0; i < hits; i++) {
				z = (1.0f + ((float) buffer.get(i*4+1) / 0x7fffffff));
				if (min < 0 || z < min) {
					minid = i;
					min = z;
				}
			}

			clickedObject = shapeList.get(buffer.get(minid*4+3));
		}

		// Reset matrixes
		gl.glMatrixMode(GL2.GL_PROJECTION);
		gl.glPopMatrix();
		gl.glMatrixMode(GL2.GL_MODELVIEW);
		gl.glLoadIdentity();
		
		return clickedObject;
	}
	
	/* ----------------------------------------------------
	 * Observer functions
	 */
	
	private void notifyObservers(){
		for(CanvasObserver observer : observers)
			observer.update(this);
	}

	public void addObserver(CanvasObserver obserber){
		observers.add(obserber);
	}
	
	/* ----------------------------------------------------
	 * Operation functions
	 */
	
	public void changeSelectedShapesProperty( double value, int index, int property ) {
		boolean doNotify = false;
		for (Shape s: selectedShapeList) {
			doNotify = s.changeProperty(value, index, property) || doNotify;
		}
		
		if (doNotify) {
			repaint();
			notifyObservers();
		}
	}
	
	public void setSelectedShapesProperty( double value, int index, int property ) {
		boolean doNotify = false;
		for (Shape s: selectedShapeList) {
			doNotify = doNotify || s.setProperty(value, index, property);
		}
		
		if (doNotify) {
			repaint();
			notifyObservers();
		}
	}
	
	public void changeCameraProperty( double value, int index, int property ) {
		if (camera.changeProperty(value, index, property)) {	
			repaint();
			notifyObservers();
		}
	}
	
	public void setCameraProperty( double value, int index, int property ) {
		if (camera.setProperty(value, index, property)) {	
			repaint();
			notifyObservers();
		}
	}
	
	/* ----------------------------------------------------
	 * Mouse functions
	 */
	
	public int getMouseX() {
		return mouse.x;
	}
	
	public int getMouseY() {
		return mouse.y;
	}
	
	public void mouseDragged(MouseEvent e) {
		int dx = e.getX() - mouse.x;
		int dy = e.getY() - mouse.y;
		
		changeCameraProperty(dx, 0, Camera.PROPERTY_YAW);
		changeCameraProperty(dy, 0, Camera.PROPERTY_PITCH);
		
		mouse.x += dx;
		mouse.y += dy;
	}

	public void mouseMoved(MouseEvent e) {
		mouse.x = e.getX();
		mouse.y = e.getY();
		
		notifyObservers();
	}

	public void mouseClicked(MouseEvent e) {}

	public void mousePressed(MouseEvent e) {
		inSelectionMode = true;
		repaint();
	}

	public void mouseReleased(MouseEvent e) {}

	public void mouseEntered(MouseEvent e) {}

	public void mouseExited(MouseEvent e) {}
	
	/* ----------------------------------------------------
	 * Canvas functions
	 */

	public void dispose(GLAutoDrawable drawable) {}

	public void init(GLAutoDrawable drawable) {
		GL2 gl = drawable.getGL().getGL2();
		Util.init();
		
		gl.glEnable(GL2.GL_DEPTH_TEST);
		gl.glEnable(GL2.GL_LIGHTING);
		gl.glEnable(GL2.GL_COLOR_MATERIAL);
		gl.glEnable(GL2.GL_NORMALIZE);
        gl.glDepthFunc(GL2.GL_LEQUAL);
        gl.glShadeModel(GL2.GL_SMOOTH);
        gl.glHint(GL2.GL_PERSPECTIVE_CORRECTION_HINT, GL2.GL_NICEST);
        
        gl.glColorMaterial( GL2.GL_FRONT_AND_BACK, GL2.GL_AMBIENT_AND_DIFFUSE );
        
        gl.glClearColor(0f, 0f, 0f, 1f);
        gl.glViewport(0, 0, getWidth(), getHeight());
	}

	public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
		GL2 gl = drawable.getGL().getGL2();
	    gl.glViewport(0, 0, width, height);
	}
	
	public void display(GLAutoDrawable drawable) {
		GL2 gl = drawable.getGL().getGL2();
		gl.glClear(GL2.GL_COLOR_BUFFER_BIT | GL2.GL_DEPTH_BUFFER_BIT);

		if (!inSelectionMode) {
			draw(gl);
		}
		else {
			lastClickedShape = findClickedObject(gl, new Point(mouse));
			inSelectionMode = false;
			
			if (lastClickedShape != null) {
				selectShape(lastClickedShape, !lastClickedShape.isSelected());
			}
			else {
				repaint();
			}
		}
	}
	
}
